/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.glass.sample.apidemo.touchpad;
import com.google.android.glass.sample.apidemo.R;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
import android.widget.TextView;
/**
* Provides a visual representation of the Glass touchpad, with colored and labeled circles that
* represent the locations of the user's fingers when they are on the touchpad.
*/
public class TouchpadView extends RelativeLayout {
private static final String TAG = TouchpadView.class.getSimpleName();
// The size of the finger trace drawn at the location of one of the user's fingers.
private static final int FINGER_TRACE_SIZE = 50;
// The drawable ids used to draw the user's three fingers.
private static final int[] FINGER_RES_IDS = {
R.drawable.finger_trace_0, R.drawable.finger_trace_1, R.drawable.finger_trace_2 };
// The duration, in milliseconds, of the animation used to fade out a finger trace when the
// user's finger is lifted from the touchpad.
private static final int FADE_OUT_DURATION_MILLIS = 100;
// The views used to display the location of a finger on the touchpad.
private final TextView[] mFingerTraceViews = new TextView[FINGER_RES_IDS.length];
// The horizontal and vertical hardware resolutions of the touchpad. These are used to
// calculate the aspect ratio of the view when it is measured.
private float mTouchpadHardwareWidth;
private float mTouchpadHardwareHeight;
public TouchpadView(Context context) {
this(context, null, 0);
}
public TouchpadView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouchpadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setFocusable(true);
setFocusableInTouchMode(true);
setClipChildren(false);
lookupTouchpadHardwareResolution();
for (int i = 0; i < mFingerTraceViews.length; i++) {
mFingerTraceViews[i] = createFingerTraceView(i);
addView(mFingerTraceViews[i]);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Constrains the view's dimensions to have the same aspect ratio as the actual hardware
// touchpad.
int newHeight = (int) (MeasureSpec.getSize(widthMeasureSpec) / mTouchpadHardwareWidth *
mTouchpadHardwareHeight);
int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
}
/**
* Processes all the pointers that are part of the motion event and displays the finger traces
* at the proper positions in the view.
* <p>
* Since this view is only intended to render motion events and not consume them, we always
* return false so that the events bubble up to the activity and the gesture detector has a
* chance to handle them.
*/
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
logVerbose(printToString(event));
for (int i = 0; i < event.getPointerCount(); i++) {
int pointerId = event.getPointerId(i);
float x = event.getX(i);
float y = event.getY(i);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_MOVE:
moveFingerTrace(pointerId, x, y);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
hideFingerTrace(pointerId);
break;
}
}
return false;
}
/** Looks up the hardware resolution of the Glass touchpad. */
private void lookupTouchpadHardwareResolution() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int deviceId : deviceIds) {
InputDevice device = InputDevice.getDevice(deviceId);
if ((device.getSources() & InputDevice.SOURCE_TOUCHPAD) != 0) {
logVerbose("Touchpad motion range: x-axis [%d, %d] y-axis [%d, %d]",
device.getMotionRange(MotionEvent.AXIS_X).getMin(),
device.getMotionRange(MotionEvent.AXIS_X).getMax(),
device.getMotionRange(MotionEvent.AXIS_Y).getMin(),
device.getMotionRange(MotionEvent.AXIS_Y).getMax());
mTouchpadHardwareWidth = device.getMotionRange(MotionEvent.AXIS_X).getRange();
mTouchpadHardwareHeight = device.getMotionRange(MotionEvent.AXIS_Y).getRange();
// Stop after we've seen the first touchpad device, because there might be multiple
// devices in this list if the user is currently screencasting with MyGlass. The
// first one will always be the hardware touchpad.
break;
}
}
}
/**
* Creates a new view that will be used to display the specified pointer id on the touchpad
* view.
*
* @param pointerId the id of the pointer that this finger trace view will represent; used to
* determine its color and text
* @return the {@code TextView} that was created
*/
private TextView createFingerTraceView(int pointerId) {
TextView fingerTraceView = new TextView(getContext());
fingerTraceView.setBackgroundResource(FINGER_RES_IDS[pointerId]);
fingerTraceView.setText(Integer.toString(pointerId));
fingerTraceView.setGravity(Gravity.CENTER);
fingerTraceView.setTextAppearance(getContext(),
android.R.style.TextAppearance_DeviceDefault_Small);
fingerTraceView.setAlpha(0);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FINGER_TRACE_SIZE, FINGER_TRACE_SIZE);
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
// The right and bottom margin here are required so that the view doesn't get "squished"
// as it touches the right or bottom side of the touchpad view.
lp.rightMargin = -2 * FINGER_TRACE_SIZE;
lp.bottomMargin = -2 * FINGER_TRACE_SIZE;
fingerTraceView.setLayoutParams(lp);
return fingerTraceView;
}
/**
* Moves the finger trace associated with the specified pointer id to a new location in the
* view.
*
* @param pointerId the pointer id of the finger trace to move
* @param point the new location of the finger trace
*/
private void moveFingerTrace(int pointerId, float x, float y) {
TextView fingerTraceView = mFingerTraceViews[pointerId];
// Cancel any current animations on the view and bring it back to full opacity.
fingerTraceView.animate().cancel();
fingerTraceView.setScaleX(1.0f);
fingerTraceView.setScaleY(1.0f);
fingerTraceView.setAlpha(1);
// Reposition the finger trace by updating the layout margins of its view.
RelativeLayout.LayoutParams lp =
(RelativeLayout.LayoutParams) fingerTraceView.getLayoutParams();
int viewX = (int) (x / mTouchpadHardwareWidth * getWidth());
int viewY = (int) (y / mTouchpadHardwareHeight * getHeight());
lp.leftMargin = viewX - FINGER_TRACE_SIZE / 2;
lp.topMargin = viewY - FINGER_TRACE_SIZE / 2;
fingerTraceView.setLayoutParams(lp);
}
/**
* Hides the finger trace associated with the specified pointer id. Traces are faded away
* instead of immediately hidden in order to reduce flickering due to intermittence in the
* touchpad.
*
* @param pointerId the pointer id whose finger trace should be hidden
*/
private void hideFingerTrace(int pointerId) {
TextView fingerTraceView = mFingerTraceViews[pointerId];
fingerTraceView.animate()
.scaleX(3.0f)
.scaleY(3.0f)
.setDuration(FADE_OUT_DURATION_MILLIS).alpha(0);
}
/**
* Helper method to debug the motion events.
*/
private static String printToString(MotionEvent ev) {
int historySize = ev.getHistorySize();
int pointerCount = ev.getPointerCount();
StringBuilder resultString = new StringBuilder();
for (int h = 0; h < historySize; h++) {
resultString.append(String.format("At time %d:", ev.getHistoricalEventTime(h)));
for (int p = 0; p < pointerCount; p++) {
resultString.append(String.format(" pointer %d %s: (%f, %f)",
ev.getPointerId(p), MotionEvent.actionToString(ev.getActionMasked()),
ev.getHistoricalX(p, h), ev.getHistoricalY(p, h)));
}
}
resultString.append(String.format("At time %d:", ev.getEventTime()));
for (int p = 0; p < pointerCount; p++) {
resultString.append(String.format(" pointer %d %s: (%f, %f)",
ev.getPointerId(p), MotionEvent.actionToString(ev.getActionMasked()),
ev.getX(p), ev.getY(p)));
}
return resultString.toString();
}
/**
* Helper method to print to the main logcat stream.
* <p>
* In order to turn the debugging on, execute following command.
* <pre> $ adb shell setprop log.tag.[TAG] VERBOSE </pre>
*/
private void logVerbose(String format, Object... args) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, String.format(format, args));
}
}
}